/*
 * RouteOps.java
 *
 * Created on January 9, 2007, 9:00 PM
 *
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fpdev.apps.admin;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.JOptionPane;
import org.fpdev.core.basenet.BLink;
import org.fpdev.core.basenet.BNode;
import org.fpdev.core.basenet.Path;
import org.fpdev.core.transit.*;
import org.fpdev.apps.admin.actions.AddStopAction;
import org.fpdev.apps.admin.actions.ApplyDiversionPathAction;
import org.fpdev.apps.admin.actions.ACAction;
import org.fpdev.apps.admin.actions.ClipCircuitDiversionAction;
import org.fpdev.apps.admin.actions.ClipRouteEndAction;
import org.fpdev.apps.admin.actions.ClipRouteStartAction;
import org.fpdev.apps.admin.actions.DeleteRouteAction;
import org.fpdev.apps.admin.actions.DeleteStopAction;
import org.fpdev.apps.admin.actions.DeleteSubRouteAction;
import org.fpdev.apps.admin.actions.ToggleTransitPathLinks;
import org.fpdev.apps.admin.gui.ACGUI;
import org.fpdev.apps.admin.gui.dialogs.ImportPathDialog;
import org.fpdev.apps.admin.gui.MapModeToolbar;
import org.fpdev.apps.admin.gui.dialogs.SelectPLIDialog;
import org.fpdev.apps.admin.gui.dialogs.InputTextDialog;
import org.fpdev.apps.admin.gui.map.MapDrawItems;

/**
 *
 * @author demory
 */
public class RouteOps {

  private AdminClient ac_;
  private Routes routes_;
  private SubRoute activeSub_;
  private Path diversionPath_;
  private RouteShpImporter shpImporter_;

  /** Creates a new instance of CRouteManager */
  public RouteOps(Routes routes, AdminClient ac) {
    routes_ = routes;
    ac_ = ac;
    shpImporter_ = new RouteShpImporter(ac);
    initDiversionPath();
  }

  public Routes getRoutes() {
    return routes_;
  }

  public void setRoutes(Routes routes) {
    routes_ = routes;
  }

  public RouteShpImporter getShpImporter() {
    return shpImporter_;
  }

  public static boolean isRouteEvent(ACEvent e) {
    return e.getType() / 100 % 10 == 4; // true if type id = X4XX
  }

  @SuppressWarnings("unchecked")
  public void handleRouteEvent(ACEvent e) {

    switch (e.getType()) {

      // Route Map Click Events

      case EventTypes.MCLICK_RTE_TOGGLE_LINK:
        toggleTransitPathLinks((List<BLink>) e.getProperty("links"));
        break;

      case EventTypes.MCLICK_RTE_TOGGLE_DIVLINK:
        toggleDiversionPathLink((List<BLink>) e.getProperty("links"));
        break;

      case EventTypes.MCLICK_RTE_CLIP_START:
        clipRoutePathStart(e.getNodeProperty("node"));
        break;

      case EventTypes.MCLICK_RTE_CLIP_END:
        clipRoutePathEnd(e.getNodeProperty("node"));
        break;

      case EventTypes.MCLICK_RTE_CLIP_DIVERSION:
        clipCircuitDiversion(e.getNodeProperty("node"));
        break;

      case EventTypes.MCLICK_RTE_ADD_STOP:
        addStop(e.getNodeProperty("node"), false);
        break;

      case EventTypes.MCLICK_RTE_DELETE_STOP:
        deleteStop(e.getNodeProperty("node"), false);
        break;

      case EventTypes.MCLICK_RTE_ADD_TIMEPT:
        addStop(e.getNodeProperty("node"), true);
        break;

      case EventTypes.MCLICK_RTE_DELETE_TIMEPT:
        deleteStop(e.getNodeProperty("node"), true);
        break;

      // Route Selection Panel

      case EventTypes.RTEPANEL_NEW_PROVIDER:
        newProvider();
        break;
      
      case EventTypes.RTEPANEL_NEW_ROUTE:
        userNewRoute();
        break;

      case EventTypes.RTEPANEL_EDIT_RTE_ID:
        editRouteID();
        break;

      // Import Route from Shapefile Events

      case EventTypes.RTESHP_SHOW_ITEM:
        getShpImporter().showImportRouteShpItem(e.getStringProperty("id"));
        break;

      case EventTypes.RTESHP_RESOLVE_ITEM:
        getShpImporter().resolveImportRouteShpItem(e.getStringProperty("id"), e.getDoubleProperty("radius"));
        break;

      case EventTypes.RTESHP_STARTNODE:
        if (e.getBooleanProperty("aborting")) {
          ac_.getGUI().clearSpecialClickAction();
          ac_.getGUI().getMapPanel().clearNodeHoverListener();
          ac_.getGUI().getMapPanel().getDrawItems().removeItem(MapDrawItems.IMP_ROUTE_HIGHLIGHT_ENDPOINT);
          ac_.getGUI().msg("Aborted start node selection");
        } else {
          getShpImporter().selectingStart();
        }
        break;

      case EventTypes.RTESHP_ENDNODE:
        if (e.getBooleanProperty("aborting")) {
          ac_.getGUI().clearSpecialClickAction();
          ac_.getGUI().getMapPanel().clearNodeHoverListener();
          ac_.getGUI().getMapPanel().getDrawItems().removeItem(MapDrawItems.IMP_ROUTE_HIGHLIGHT_ENDPOINT);
          ac_.getGUI().msg("Aborted end node selection");
        } else {
          getShpImporter().selectingEnd();
        }
        break;

      case EventTypes.RTESHP_EDIT_LINKS:
        if (e.getBooleanProperty("ending")) {
          ac_.getGUI().clearSpecialClickAction();
        } else {
          getShpImporter().editingLinks();
        }
        break;

      case EventTypes.RTESHP_SPECIFY_PATH:
        if (e.getBooleanProperty("ending")) {
          getShpImporter().stopSpecifyingPath();
        } else {
          getShpImporter().specifyingPath();
        }
        break;

      case EventTypes.RTESHP_APPLY_CURRENT:
        getShpImporter().applyToCurrent();
        break;

      case EventTypes.RTESHP_CLEAR_CURRENT:
        System.out.println("clearing current");
        getShpImporter().clearCurrent();
        ac_.getGUI().getMapPanel().refresh(false, false, true);
        break;


      // Route Menu Events

      case EventTypes.RTEMENU_IMPORT_NAMES_TEXT:
        importRouteNamesFromText();
        break;

      case EventTypes.RTEMENU_AUTOLOAD_TIMETABLES:
        autoLoadTimeTables();
        break;

      case EventTypes.RTEMENU_CLEAR_TIMETABLES:
        clearTimetables();
        break;

      case EventTypes.RTEMENU_IMPORT_PATH_INTERNAL:
        importRoutePath();
        break;

      case EventTypes.RTEMENU_IMPORT_SHP:
        getShpImporter().importRoutesFromShapefile();
        break;

      case EventTypes.RTEMENU_SHOW_IMPORT_SHP_FRAME:
        ac_.getGUI().getImportRouteShpFrame().setVisible(true);
        break;

      case EventTypes.RTEMENU_IMPORT_GTFS:
        //new ImportGTFSDialog(av_.getGUI());
        break;

    }
  }

  public void setActiveSubRoute(SubRoute sub) {
    activeSub_ = sub;
    if (ac_.getGUI() == null) {
      return;
    }
    if (sub == null) {
      ac_.getGUI().getMapPanel().getDrawItems().removeItem(MapDrawItems.RTE_TRANSIT_PATH);
    } //av_.getGUI().setTPath(null);
    else {
      //System.out.println("setting gui tpath w/ len="+sub.getPath().linkCount());
      //av_.getGUI().setTPath(sub.getPath());
      if (ac_.getGUI().getCurrentClickAction() == MapModeToolbar.ACT_RPATH) {
        ac_.getGUI().getMapPanel().setBaseTraceLinks(sub.getPath().getLinkSet());
        ac_.getGUI().getMapPanel().setTraceDestNodes(sub.getPath().getEndPointSet());
      }
      ac_.getGUI().getMapPanel().getDrawItems().addItem(MapDrawItems.RTE_TRANSIT_PATH, sub.getPath(), "r", 20);
      ac_.getGUI().getMapPanel().refresh(false, false, true);
      updateTimetableInGUI();
    }
  //av_.getGUI().getTPathOptionsPanel().setTypeEnabled(sub!=null);
  }

  public SubRoute getActiveSubRoute() {
    return activeSub_;
  }
  private BNode iRPfT_1st_ = null;
  private BNode iRPfT_2nd_ = null;
  private String iRPfT_text_ = "";

  public void importRoutePathFromText() {
    if (activeSub_ == null) {
      ac_.msg("No active subroute");
      return;
    }
    if (iRPfT_1st_ != null && iRPfT_2nd_ != null) {
      int result = JOptionPane.showConfirmDialog(ac_.getGUI(), "Reuse last input nodes?");
      if (result == JOptionPane.YES_OPTION) {
        importRoutePathFromText_2ndNode(iRPfT_2nd_);
        return;
      } else {
        iRPfT_text_ = "";
      }
    }
    //JOptionPane.showMessageDialog(av_.getGUI(), "Please select origin node.");
    ac_.msg("Select origin node");
    ac_.getGUI().setSpecialClickAction(ACGUI.SPECIAL_SELNODE_IMPTEXT_1ST);
  }

  public void importRoutePathFromText_1stNode(BNode first) {
    iRPfT_1st_ = first;
    //JOptionPane.showMessageDialog(av_.getGUI(), "Select second node to indicate initial direction.");
    ac_.msg("Select second node");
  }

  public void importRoutePathFromText_2ndNode(BNode second) {
    BNode first = iRPfT_1st_;
    iRPfT_2nd_ = second; // store for possible later use
    if (!ac_.getEngine().getBaseNet().areAdjacent(first, second)) {
      ac_.msg("Selected nodes must be adjacent");
      return;
    }

    InputTextDialog itd = new InputTextDialog(ac_, "Enter path direction text:", iRPfT_text_);
    if (itd.okPressed()) {
      iRPfT_text_ = itd.getText();
      boolean success = activeSub_.getPath().importPathFromText(first, second, itd.getText(), ac_.getGUI().getTPathOptionsPanel().getAddStops());
      if (success) {
        ac_.getGUI().getMapPanel().updated(false, false, true);
        ac_.getGUI().getMapPanel().repaint();
      }
    }
  }

  public void writeRoutesFile() {

    ac_.getGUI().statusText("Writing routes file");
    routes_.writeToXMLFile(ac_.getGUI());
    ac_.getGUI().statusText("");
    ac_.getGUI().setProgress(0);
  }

  public void generateXMLForWeb() {
    ac_.getEngine().getRoutes().generateXMLForWeb();
    ac_.msg("Wrote files to data/" + ac_.getEngine().getDataPackage().getName() + "/web");
  }

  // route table methods 
  /*public void loadRouteTableFromDBF() {
    if (activeSub_ == null) {
      av_.msg("No active subroute");
      return;
    }
    JFileChooser chooser = new JFileChooser();
    String startDir = av_.getEngine().getProperty("5pHome") + "data" + File.separator + av_.getEngine().getDataPackage().getName() + File.separator + "route" + File.separator;
    System.out.println("sDir: " + startDir);
    chooser.setCurrentDirectory(new File(startDir));

    int returnVal = chooser.showOpenDialog(av_.getGUI());
    if (returnVal == JFileChooser.APPROVE_OPTION) {
      av_.getEngine().getRoutes().readTimeTableFromFile(chooser.getSelectedFile(), activeSub_, av_.getGUI().getTablePanel().getSelectedDay(), av_.getGUI());
      updateTimetableInGUI(true);
      activeSub_.getTable(av_.getGUI().getTablePanel().getSelectedDay()).setFilename(chooser.getSelectedFile().getName());
    //av_.getGUI().getTablePanel().updateTimetableInGUI(activeSub_.getTableModel(av_.getGUI().getTablePanel().getIncludeAllStops()));
    }
  }*/

  public void clearTimetables() {
    Route rte = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute();
    
    if (rte == null) return;

    for(SubRoute sub : rte.getSubRoutes()) sub.clearTables();
    
    updateTimetableInGUI();

    ac_.msg("Cleared timetables for route " + rte.getID());
    ac_.getRouteOps().getRoutes().changesMade();
    int selRte = ac_.getGUI().getRouteSelectionPanel().getSelectedRouteIndex();
    ac_.getGUI().getRouteSelectionPanel().updateRoutes(true);
    ac_.getGUI().getRouteSelectionPanel().selectRouteByIndex(selRte);
    //ac_.getGUI().getRouteSelectionPanel().updateSubs();
  }

  public void updateTimetableInGUI() {
    updateTimetableInGUI(false);
  }

  public void updateTimetableInGUI(boolean recalc) {
    if (activeSub_ == null) {
      return;
    }
    if (recalc) {
      activeSub_.calculateBasicStopTimes();
    }
    ac_.getGUI().getTablePanel().updateSubRoute(activeSub_);
    //av_.getGUI().getTablePanel().updateTable(activeSub_.getTableModel(av_.getGUI().getTablePanel().getSelectedDay(), av_.getGUI().getTablePanel().getIncludeAllStops()));
  }

  public void newProvider() {
    String id = JOptionPane.showInputDialog("Provider ID:");
    String name = JOptionPane.showInputDialog("Provider Name:");
    if (id == null || id.length() == 0 || name == null || name.length() == 0) {
      return;
    }
    routes_.getProviders().newProvider(id, name);
    ac_.getRouteOps().getRoutes().changesMade();
    ac_.getGUI().getRouteSelectionPanel().updateProviders();
  }

  public void userNewRoute() {
    String id = JOptionPane.showInputDialog("Please Specify Route ID:");
    if (id != null && id.length() > 0) {
      Route rte = newRoute(id, "");
    
      if(rte != null) {
        ac_.getGUI().getRouteSelectionPanel().updateRoutes(false);
        ac_.getGUI().getRouteSelectionPanel().selectRouteByID(rte.getID());
      }
    }
  }

  public Route newRoute(String id, String name) {
    String combID = ac_.getGUI().getRouteSelectionPanel().getSelectedProvider().getID() + id;
    if (routes_.routeExists(combID)) {
      if (routes_.getRoute(combID).getScenario().isAncestorOf(ac_.getActiveScenario())) {
        ac_.msg("override case");
        id = id + "~" + ac_.getActiveScenario().getID();
      } else {
        ac_.msg("Route w/ id '" + id + "' already exists");
        return null;
      }
    }
    Route route = new Route(ac_.getActiveScenario(), ac_.getGUI().getRouteSelectionPanel().getSelectedProvider(), id, name);
    routes_.add(route);
    int rank = 0;
    for(Route rte : routes_.getCollectionSortedByID()) {
      if (rte.getID().equals(id)) break;
      rank++;
    }
    ac_.getRouteOps().getRoutes().changesMade();
    return route;
  }

  public void deleteRoute(String id) {
    Route route = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute();
    ACAction action = new DeleteRouteAction(route.getCombinedID());
    if (action.doAction(ac_)) {
      ac_.getHistory().addAction(action);
    }
  }

  /*public void deleteRoute(String combID) {
  routes_.getRoute(combID).clearSubRoutes(av_.getEngine().getRouteNetwork());
  routes_.getMap().remove(combID);
  //av_.getGUI().getRouteSelectionPanel().updateRoutes();
  }*/
  public void editRouteID() {
    Route rte = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute();
    if (rte == null) {
      return;
    }
    String newID = JOptionPane.showInputDialog("New ID:");
    if (newID == null || newID.length() == 0) {
      return;
    }
    routes_.updateID(rte, newID);
    ac_.getGUI().getRouteSelectionPanel().updateRoutes();
  }

  public int newSubRoute() {
    return newSubRoute(JOptionPane.showInputDialog("Specify subroute ID:"));
  }

  public int newSubRoute(String id) {
    int index = -1;
    if (id != null && id.length() > 0) {
      Route route = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute();
      SubRoute sub = new SubRoute(route, id, ac_.getEngine(), ac_.getGUI().getTPathOptionsPanel().getSelectedType());
      index = route.addSubRoute(sub);
      ac_.getRouteOps().getRoutes().changesMade();
      //av_.getGUI().getRouteSelectionPanel().updateSubs();
    }
    return index;
  }

  public void deleteSubRoute(String id) {
    Route route = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute();
    ACAction action = new DeleteSubRouteAction(route, id);
    if (action.doAction(ac_)) {
      ac_.getHistory().addAction(action);
    }
  }

  public void autoAddSubRoutes(String[] items, String suffix) {
    if (suffix == null) {
      return;
    }
    boolean copyPaths = false;
    if (suffix.length() > 0) {
      copyPaths = (JOptionPane.showConfirmDialog(ac_.getGUI(), "Copy route paths from base subroutes (if possible)?", "Question", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION);
    }
    for (int i = 0; i < items.length; i++) {
      String id = items[i] + (suffix.length() > 0 ? "_" + suffix : "");
      newSubRoute(id);
      if (copyPaths) {
        SubRoute base = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute().getSubRoute(items[i]);
        SubRoute newSub = base.getRoute().getSubRoute(id);
        if (base != null) {
          newSub.setPath(base.getPath().createCopy(ac_.getEngine(), newSub, false));
        }
      }
    }
    ac_.getGUI().getRouteSelectionPanel().updateSubs();

  }

  public void importRoutePath() {
    ImportPathDialog cipd = new ImportPathDialog(ac_, ac_.getGUI().getRouteSelectionPanel().getSelectedRouteIndex());
    SubRoute sub = cipd.getSelectedSubRoute();
    importRoutePath(sub, cipd.getReverse());
    /*if (sub != null) {
      activeSub_.setPath(sub.getPath().createCopy(ac_.getEngine(), activeSub_, cipd.getReverse()));
      ac_.getGUI().getMapPanel().getDrawItems().addItem(MapDrawItems.RTE_TRANSIT_PATH, activeSub_.getPath(), "r", 20);
      updateTimetableInGUI();
      ac_.getGUI().getMapPanel().refresh(false, false, true);
      ac_.getGUI().getRouteSelectionPanel().updateSubs();
    }*/
  }

  public void importRoutePath(SubRoute sub, boolean reverse) {
    if (sub == null) return;
    activeSub_.setPath(sub.getPath().createCopy(ac_.getEngine(), activeSub_, reverse));
    ac_.getGUI().getMapPanel().getDrawItems().addItem(MapDrawItems.RTE_TRANSIT_PATH, activeSub_.getPath(), "r", 20);
    updateTimetableInGUI();
    ac_.getGUI().getMapPanel().refresh(false, false, true);
    ac_.getGUI().getRouteSelectionPanel().updateSubs();
  }

  public void autoLoadTimeTables() {
    //if(activeSub_ == null) return; 
    Route rte = ac_.getGUI().getRouteSelectionPanel().getSelectedRoute();

    if (rte == null) return;
    
    ac_.msg("Auto-loading tables for route " + rte.getID());
    for(SubRoute sub : rte.getSubRoutes()) autoLoadTimeTables(sub);

    int sel = ac_.getGUI().getRouteSelectionPanel().getSelectedRouteIndex();
    ac_.getGUI().getRouteSelectionPanel().updateRoutes(true);
    ac_.getGUI().getRouteSelectionPanel().selectRouteByIndex(sel);
  }

  private void autoLoadTimeTables(SubRoute sub) {
    for(CalendarService service : routes_.getCalendarServices().getServices()) {
      if(!tryTimeTableFile(sub, service, "csv"))
        tryTimeTableFile(sub, service, "dbf");
    }

    /*if(!tryTimeTableFile(sub, Timetable.DayOfWeek.WEEKDAY, "csv"))
      tryTimeTableFile(sub, Timetable.DayOfWeek.WEEKDAY, "dbf");
    if(!tryTimeTableFile(sub, Timetable.DayOfWeek.SATURDAY, "csv"))
      tryTimeTableFile(sub, Timetable.DayOfWeek.SATURDAY, "dbf");
    if(!tryTimeTableFile(sub, Timetable.DayOfWeek.SUNDAY, "csv"))
      tryTimeTableFile(sub, Timetable.DayOfWeek.SUNDAY, "dbf");*/
    
   
  }
  
  private boolean tryTimeTableFile(SubRoute sub, CalendarService service, String filetype) {
    
    String provID = sub.getRoute().getProvider().getID();
    String subid = sub.getID().toLowerCase().replaceAll("_", "");
    String base = ac_.getEngine().getProperty("5pHome") + "data" + File.separator + ac_.getEngine().getDataPackage().getName() + File.separator + "route" + File.separator + provID + sub.getRoute().getID() + File.separator + provID + sub.getRoute().getID() + "_" + subid;
    String filename = base + "_"+service.getID()+"."+filetype;
    File file = new File(filename);
    System.out.println(filename);
    if (file.exists()) {
      ac_.msg("Found "+service.getName()+" table for " + sub.getID());
      if (ac_.getEngine().getRoutes().readTimeTableFromFile(file, sub, service, ac_.getGUI())) {
        sub.getTable(service).setFilename(file.getName());
        ac_.msg(service.getName()+" table loaded");
        updateTimetableInGUI(true);
        return true;
      }
    }
    return false;
  }

  public void clipRoutePathStart(BNode clipNode) {

    // error checking
    if (activeSub_ == null || clipNode == null) {
      return;
    }
    if (!activeSub_.getPath().visitsNode(clipNode)) {
      ac_.msg("Node is not on active route path");
      return;
    }

    // check for multiple path location options 
    TransitPath tPath = activeSub_.getPath();
    int firstPLI = tPath.getNodeVisitationCount(clipNode) == 1 ?
      tPath.getOutPLIs(clipNode).next() :
      new SelectPLIDialog(ac_.getGUI(), tPath.getOutPLIs(clipNode)).getSelectedPLI();
    
    if(firstPLI == -1) return; // dialog canceled

    
    // create and run action, then update display
    ClipRouteStartAction action = new ClipRouteStartAction(activeSub_, firstPLI);
    if (action.doAction(ac_)) {
      ac_.getHistory().addAction(action);

      ac_.getGUI().getMapPanel().getDrawItems().transitPathItemUpdated(activeSub_.getPath());
      ac_.getGUI().getMapPanel().updated(false, false, true);
      ac_.getGUI().getMapPanel().repaint();
    }
  }

  public void clipRoutePathEnd(BNode clipNode) {

    // error checking
    if (activeSub_ == null || clipNode == null) {
      return;
    }
    if (!activeSub_.getPath().visitsNode(clipNode)) {
      ac_.msg("Node is not on active route path");
      return;
    }

    // check for multiple path location options 
    TransitPath tPath = activeSub_.getPath();
    int lastPLI = tPath.getNodeVisitationCount(clipNode) == 1 ?
      tPath.getInPLIs(clipNode).next() :
      new SelectPLIDialog(ac_.getGUI(), tPath.getInPLIs(clipNode)).getSelectedPLI();
    
    if(lastPLI == -1) return; // dialog canceled
    
    // create and run action, then update display
    ClipRouteEndAction action = new ClipRouteEndAction(activeSub_, lastPLI);
    if (action.doAction(ac_)) {
      ac_.getHistory().addAction(action);
      ac_.getGUI().getMapPanel().getDrawItems().transitPathItemUpdated(activeSub_.getPath());
      ac_.getGUI().getMapPanel().updated(false, false, true);
      ac_.getGUI().getMapPanel().repaint();
    }
  }

  public void addStop(BNode node, boolean isTimePoint) {
    if (node == null) {
      return;
    }
    if (activeSub_ == null) {
      ac_.msg("No Active Subroute");
      return;
    }
    TransitPath tPath = activeSub_.getPath();

    ac_.getEngine().clearTransitAnnotations();
    int psCount = tPath.getNodeVisitationCount(node);
    int stopCount = tPath.getStopCountAtNode(node);
    int newStopPLI = -1;

    if (psCount == 0) {
      ac_.msg("No potential for stops at selected node");
    } else if (psCount == stopCount) {
      // special case: existing basic stop to be upgraded to timepoint
      if (psCount == 1 && isTimePoint && tPath.getTimePointCountAtNode(node) == 0) {

        // delete existing basic stop:
        Iterator<Integer> plis = tPath.getBasicStopInPLIs(node);
        int delStopPLI = plis.next().intValue();
        int stopIndex = tPath.getStopIndexFromInPLI(delStopPLI);
        ac_.msg("Deleting existing basic stop at PLI " + delStopPLI);
        ACAction action = new DeleteStopAction(activeSub_, stopIndex, false);
        action.doAction(ac_);
        ac_.getHistory().addAction(action);
        //tPath.deleteStop(stopIndex);

        // set timepoint initialization variables:
        plis = tPath.getAvailableStopInPLIs(node);
        newStopPLI = plis.next().intValue();
        ac_.msg("pli for addition: " + newStopPLI);
      } else {
        ac_.msg("All potential stops already exist");
      } // default case
    } else if (psCount - stopCount == 1) { // exactly one potential stop
      Iterator<Integer> plis = tPath.getAvailableStopInPLIs(node);
      newStopPLI = plis.next().intValue();
      ac_.msg("pli for addition: " + newStopPLI);
    } else { // more than 1 potential stop at selected node
      Iterator<Integer> plis = tPath.getAvailableStopInPLIs(node);
      SelectPLIDialog dialog = new SelectPLIDialog(ac_.getGUI(), plis);
      newStopPLI = dialog.getSelectedPLI();
    }

    if (newStopPLI >= 0) { // add the stop, if appropriate
      ACAction action = new AddStopAction(activeSub_, newStopPLI, isTimePoint);

      //if(tPath.addStopAfter(node, isTimePoint, newStopPLI)) {
      if (action.doAction(ac_)) {
        if (isTimePoint) {
          TimePoint addedTPt = (TimePoint) tPath.getStop(tPath.getStopIndexFromInPLI(newStopPLI));
          String defaultName = "TPT" + addedTPt.getIndex();
          String name = JOptionPane.showInputDialog("New TimePoint Name:", defaultName);
          addedTPt.setName(name != null ? name : defaultName);
        }
        ac_.getGUI().getMapPanel().refresh(false, false, true);
        updateTimetableInGUI();
        ac_.getHistory().addAction(action);
      }
    }
  }

  public void deleteStop(BNode node, boolean isTimePoint) {
    if (node == null) {
      return;
    }
    if (activeSub_ == null) {
      ac_.msg("No Active Subroute");
      return;
    }
    TransitPath tPath = activeSub_.getPath();

    ac_.getEngine().clearTransitAnnotations();
    int stopCount = isTimePoint ? tPath.getTimePointCountAtNode(node) : tPath.getBasicStopCountAtNode(node);
    int delStopPLI = -1;

    if (stopCount == 0) {
      ac_.msg("No stop at selected node");
    } else if (stopCount == 1) {
      Iterator<Integer> plis = isTimePoint ? tPath.getTimePointInPLIs(node) : tPath.getBasicStopInPLIs(node);
      delStopPLI = plis.next().intValue();
    } else {
      Iterator<Integer> plis = isTimePoint ? tPath.getTimePointInPLIs(node) : tPath.getBasicStopInPLIs(node);
      SelectPLIDialog dialog = new SelectPLIDialog(ac_.getGUI(), plis);
      delStopPLI = dialog.getSelectedPLI();
    }

    if (delStopPLI >= 0) {
      int stopIndex = tPath.getStopIndexFromInPLI(delStopPLI);
      ACAction action = new DeleteStopAction(activeSub_, stopIndex, isTimePoint);
      if (action.doAction(ac_)) {
        ac_.getGUI().getMapPanel().refresh(false, false, true);
        updateTimetableInGUI();
        ac_.getHistory().addAction(action);
      }
    }
  }

  public void toggleTransitPathLinks(List<BLink> links) {
    if (activeSub_ == null) {
      ac_.msg("No Active Subroute");
      return;
    }

    TransitPath tPath = activeSub_.getPath();

    // check scenario compatibility
    Iterator<BLink> linkIter = links.iterator();
    while (linkIter.hasNext()) {
      BLink link = linkIter.next();
      if (!tPath.checkScenario(link.getScenario())) {
        ac_.msg("Scenario incompatibility for link " + link.getID());
        return;
      }
    }


    ToggleTransitPathLinks action =
            new ToggleTransitPathLinks(activeSub_, links, ac_.getGUI().getTPathOptionsPanel().getAddStops());

    if (action.doAction(ac_)) {

      if (action.getOldStartNode() != tPath.startNode() && ac_.getGUI().getMapPanel().getCanvas().getCC().pointNearEdge(tPath.startNode().toPoint(), 50)) {
        ac_.getGUI().getMapPanel().recenter(tPath.startNode());
      } else if (action.getOldEndNode() != tPath.endNode() && ac_.getGUI().getMapPanel().getCanvas().getCC().pointNearEdge(tPath.endNode().toPoint(), 50)) {
        ac_.getGUI().getMapPanel().recenter(tPath.endNode());
      } else {
        ac_.getGUI().getMapPanel().refresh(false, false, true);
      }

      ac_.getGUI().getMapPanel().setBaseTraceLinks(tPath.getLinkSet());
      ac_.getGUI().getMapPanel().setTraceDestNodes(tPath.getEndPointSet());
      ac_.getGUI().getMapPanel().clearHover();
      ac_.getGUI().getRouteSelectionPanel().updateSubs();

      ac_.getHistory().addAction(action);
      System.out.println("added action");
    }
  }

  public void initDiversionPath() {
    diversionPath_ = new Path(ac_.getEngine());
    diversionPath_.setDefaultDrawColor(java.awt.Color.RED);
    diversionPath_.setDefaultDrawArrows(true);
  }

  public Path getDiversionPath() {
    return diversionPath_;
  }

  /**
   * Toggles one or more links in the current diversion path. Assuming the links
   * paremeter represents a valid modification to the path, one of the following
   * three things will happen:
   * <ol>
   * <li> One or more link(s) will be appended to the diversion path, without 
   * returning to the main path.
   * <li> A single leading or trailing link will be removed from the path.  
   * <li> One or more link(s) will be appended to the diversion path such that
   * the diversion is completed. In this case, the diversion (provided it 
   * is valid), will be incorporated into the main path.
   * </ol>
   * <p>Note that an ApplyDiversionPathAction is generated and added to the
   * history if and only if case (3) occurs and results in a successful
   * incorporation. Intermediate edits to the diversion path routing are not
   * undo/redo-able actions.
   *
   * @param links the series of one or more links to toggle
   */
  public void toggleDiversionPathLink(List<BLink> links) {
    if (activeSub_ == null) {
      ac_.msg("No Active Subroute");
      return;
    }
    TransitPath tPath = activeSub_.getPath();

    if (links.size() == 1) {
      // try removal
      if (diversionPath_.containsLink(links.get(0))) {
        if (diversionPath_.endLink() == links.get(0)) {
          diversionPath_.removeLastLink();
          ac_.getGUI().getMapPanel().refresh(false, false, true);
          return;
        }
      /*else {
      av_.msg("Only the end link can be removed");
      return;
      }*/
      }

    }

    Iterator<BLink> linkIter = links.iterator();
    while (linkIter.hasNext()) {

      BLink link = linkIter.next();

      if (link.getID() < 0) {
        ac_.msg("Cannot add transit d-link");
        return;
      }

      if (diversionPath_.linkCount() == 0) { // first link

        if (tPath.visitsNode(link.getFNode())) {
          diversionPath_.addDirectedFirstLink(link, link.getFNode());
        } // addLink(link);   						
        else if (tPath.visitsNode(link.getTNode())) {
          diversionPath_.addDirectedFirstLink(link, link.getTNode());
        } else {
          ac_.msg("1st link must touch transit path");
        }
      } else { // second or more
        if (diversionPath_.isAdjacent(link)) {
          diversionPath_.addToEnd(link);
        } else { // non-adjacency
          Set<BNode> nodeSet = new HashSet<BNode>();
          //nodeSet.add(diversionPath_.startNode());
          nodeSet.add(diversionPath_.endNode());
          Iterator<BLink> adjPath = ac_.getEngine().getBaseNet().findLinksToNodeSet(link, nodeSet);
          if (adjPath != null) {
            while (adjPath.hasNext()) {
              BLink alink = adjPath.next();
              diversionPath_.addToEnd(alink);
            }
          } else {
            ac_.msg("Could not find path to link");
          }
        }

      }

      if (tPath.visitsNode(diversionPath_.endNode())) {
        ac_.msg("Returned to path");

        // create the action
        ACAction action = new ApplyDiversionPathAction(activeSub_, diversionPath_, ac_.getGUI().getTPathOptionsPanel().getAddStops());
        if (action.doAction(ac_)) {
          ac_.getHistory().addAction(action);
        }

        // clean up
        initDiversionPath();
        ac_.getGUI().getMapPanel().getDrawItems().removeItem(MapDrawItems.DIVERSION_PATH);
        ac_.getGUI().getMapPanel().clearTraceHoverPath();
        ac_.getGUI().getRouteSelectionPanel().updateSubs();
      } else {
        //System.out.println("adding links starting w/ "+diversionPath_.getLinkSet().iterator().next().getID());
        ac_.getGUI().getMapPanel().addBaseTraceLinks(diversionPath_.getLinkSet());
        //System.out.println("adding t-dest node "+diversionPath_.endNode().getID());
        ac_.getGUI().getMapPanel().setTraceDestNodes(Collections.singleton(diversionPath_.endNode()));
      }

      ac_.getGUI().getMapPanel().getDrawItems().addItem(MapDrawItems.DIVERSION_PATH, diversionPath_, "r");
      ac_.getGUI().getMapPanel().refresh(false, false, true);
    }
  }

  /**
   * Cancels the active diversion path. Clears both RouteOps and MapPanel 
   * references to the path, resets the MapPanel's trace hover path, and 
   * refreshes the map display.
   */
  public void cancelDiversionPath() {
    initDiversionPath();
    ac_.getGUI().getMapPanel().clearTraceHoverPath();
    ac_.getGUI().getMapPanel().getDrawItems().removeItem(MapDrawItems.DIVERSION_PATH);
    ac_.getGUI().getMapPanel().refresh(true, false, true);
  }

  /**
   * Clips a "circuit" diversion, a route feature where a detour both leaves and
   * rejoins the main path at a common node. The circuit is removed in full, and
   * the main "trunk" path is unaffected.
   *  
   * @param node the node at which the circuit leaves and rejoins the path
   */
  public void clipCircuitDiversion(BNode node) {
    if (node == null) {
      return;
    }
    if (activeSub_ == null) {
      ac_.msg("No Active Subroute");
      return;
    }

    ACAction action = new ClipCircuitDiversionAction(activeSub_, node);
    if (action.doAction(ac_)) {
      ac_.getHistory().addAction(action);
    }

    ac_.getGUI().getMapPanel().refresh(false, false, true);

  }

  public void importRouteNamesFromText() {
    InputTextDialog textDialog = new InputTextDialog(ac_, "Route ids/names:", "");
    if (!textDialog.okPressed()) {
      return;
    }
    Provider prov = ac_.getGUI().getRouteSelectionPanel().getSelectedProvider();
    try {
      BufferedReader reader = new BufferedReader(new StringReader(textDialog.getText()));
      int lineNum = 1, routesCreated = 0;
      while (reader.ready()) {
        String line = reader.readLine();
        if (line == null) {
          break;
        }

        String[] split = line.split(" ", 2);
        String id = split[0].trim();
        String name = split.length > 1 ? split[1].trim() : "";
        //System.out.println(id+": "+name);
        if (routes_.routeExists(prov.getID() + id)) {
          ac_.msg("Route " + id + " exists for " + prov.getName());
        } else {
          newRoute(id, name);
          routesCreated++;
        }

        lineNum++;
      }
      reader.close();
      ac_.msg(routesCreated + " routes created");
      ac_.getGUI().getRouteSelectionPanel().updateRoutes(false);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}
